// gitdaemon
package main

import (
	"crypto/tls"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"time"
)

var value [1024]int

var (
	checkurl    = flag.String("checkurl", "https://127.0.0.1/api/", "httpcheck api")
	filelog     = flag.String("logfile", "/var/log/gitdaemon.log", "log place")
	realfiledir = flag.String("repdir", "/home/git/repositories/", "repositories place")
	checkmod    = flag.String("checkmod", "file", "check mod")
)

func main() {
	flag.Parse()

	if *checkmod != "https" && *checkmod != "file" {
		fmt.Println("wrong checkmod")
		os.Exit(1)
	}

	var service_count int

	tcpAddr, err := net.ResolveTCPAddr("tcp4", "0.0.0.0:9418")
	if err != nil {
		os.Exit(1)
	}

	//start_service
	listener, err := net.ListenTCP("tcp4", tcpAddr)
	if err != nil {
		os.Exit(1)
	}

	conn := make([]net.Conn, 1024)

	for {
		if value[service_count] > 0 {
			time.Sleep(100 * time.Millisecond)
		} else {
			conn[service_count], err = listener.Accept()

			if err != nil {
				log_err(err)
				time.Sleep(100 * time.Millisecond)
				continue
			}

			go handle_connect(conn[service_count], service_count)
			value[service_count] = 1
		}

		//only allow 1000 conn
		service_count++
		if service_count > 999 {
			service_count = 0
		}
	}

	os.Exit(1)
}

func check_read(dir string) int {
	dirfile := dir + "/git-daemon-export-ok"
	conf, err := os.Open(dirfile)
	if err != nil {
		return -1
	}
	conf.Close()
	return 0
}

func get_command(buf [1024]byte, totallen int) (string, int) {
	var i int

	for i = 4; i < totallen; i++ {
		if buf[i] == 0 {
			break
		}
	}
	if i >= totallen {
		return "", -1
	}

	if strings.Contains(string(buf[4:20]), "git-upload-pack") != true {
		return "", -1
	}
	if strings.Contains(string(buf[i-4:i]), ".git") != true {
		return "", -1
	}

	filedir := *realfiledir + string(buf[20:i])

	tag := check_read(filedir)
	if tag < 0 {
		return "", -1
	}

	return filedir, 0
}

func check_read_http(dir string) int {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	client := &http.Client{Transport: tr}

	res, err := client.Get(dir)
	if err != nil {
		return -1
	}
	if res.StatusCode != 200 {
		return -1
	}

	robots, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return -1
	}

	res.Body.Close()

	if string(robots) == "true" {
		return 0
	}

	return -1
}

func get_command_http(buf [1024]byte, totallen int) (string, int) {
	var i int

	for i = 4; i < totallen; i++ {
		if buf[i] == 0 {
			break
		}
	}
	if i >= totallen {
		return "", -1
	}

	if strings.Contains(string(buf[4:20]), "git-upload-pack") != true {
		return "", -1
	}
	if strings.Contains(string(buf[i-4:i]), ".git") != true {
		return "", -1
	}

	filedir := *realfiledir + string(buf[21:i])

	httpaddr := *checkurl + string(buf[21:i-4])

	tag := check_read_http(httpaddr)
	if tag < 0 {
		return "", -1
	}

	return filedir, 0
}

func handle_connect(conn net.Conn, count int) {
	var readbuf [1024]byte
	conn.SetReadDeadline(time.Now().Add(time.Duration(60e9)))
	n, err := conn.Read(readbuf[0:])
	if err != nil {
		conn.Close()
		value[count] = 0
		return
	}

	var zero time.Time
	conn.SetReadDeadline(zero)

	real_command := ""
	tag := -1

	if n > 1000 || n < 21 {
		conn.SetWriteDeadline(time.Now().Add(time.Duration(10e9)))
		_, err = conn.Write([]byte("0019ERR Permission denied"))
		conn.Close()
		value[count] = 0
		return
	}

	if *checkmod == "https" {
		real_command, tag = get_command_http(readbuf, n)
	} else if *checkmod == "file" {
		real_command, tag = get_command(readbuf, n)
	}

	if tag < 0 {
		conn.SetWriteDeadline(time.Now().Add(time.Duration(10e9)))
		_, err = conn.Write([]byte("0019ERR Permission denied"))
		conn.Close()
		value[count] = 0
		return
	}

	cmd := exec.Command("git-upload-pack", "--strict", "--timeout=60", real_command)

	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log_err(err)
		conn.Close()
		value[count] = 0
		return
	}
	stdin, err := cmd.StdinPipe()
	if err != nil {
		log_err(err)
		conn.Close()
		value[count] = 0
		return
	}

	go io.Copy(stdin, conn)

	err = cmd.Start()
	if err != nil {
		conn.Close()
		log_err(err)
		value[count] = 0
		return
	}
	for {
		j, err := io.CopyN(conn, stdout, 1024)
		if err != nil {
			break
		}
		if j < 1024 {
			break
		}
	}

	err = cmd.Wait()
	if err != nil {
		log_err(err)
	}

	conn.Close()
	value[count] = 0
}

func log_err(err error) {
	daytime := time.Now().String()

	logFile, err1 := os.OpenFile(*filelog, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
	if err1 != nil {
		fmt.Fprintf(os.Stderr, "%s", err.Error())
	}

	logFile.WriteString(daytime + ":" + err.Error() + "\n")
	logFile.Close()
}
